// Copyright 2017 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "sdmmc-block-device.h"

#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <ddk/debug.h>
#include <ddk/device.h>
#include <ddk/protocol/sdmmc.h>
#include <hw/sdmmc.h>
#include <pretty/hexdump.h>

namespace {

constexpr uint32_t kFreq200MHz = 200'000'000;
constexpr uint32_t kFreq52MHz = 52'000'000;
constexpr uint32_t kFreq25MHz = 25'000'000;

constexpr uint64_t kMmcSectorSize = 512;  // physical sector size
constexpr uint64_t kMmcBlockSize = 512;   // block size is 512 bytes always because it is the
                                          // required value if the card is in DDR mode

}  // namespace

namespace {

zx_status_t DecodeCid(const uint8_t* raw_cid) {
  zxlogf(INFO, "mmc: product name=%c%c%c%c%c%c\n", raw_cid[MMC_CID_PRODUCT_NAME_START],
         raw_cid[MMC_CID_PRODUCT_NAME_START + 1], raw_cid[MMC_CID_PRODUCT_NAME_START + 2],
         raw_cid[MMC_CID_PRODUCT_NAME_START + 3], raw_cid[MMC_CID_PRODUCT_NAME_START + 4],
         raw_cid[MMC_CID_PRODUCT_NAME_START + 5]);
  zxlogf(INFO, "       revision=%u.%u\n", (raw_cid[MMC_CID_REVISION] >> 4) & 0xf,
         raw_cid[MMC_CID_REVISION] & 0xf);
  zxlogf(INFO, "       serial=%u\n", *reinterpret_cast<const uint32_t*>(&raw_cid[MMC_CID_SERIAL]));
  return ZX_OK;
}

zx_status_t DecodeCsd(const uint8_t* raw_csd) {
  uint8_t spec_vrsn = (raw_csd[MMC_CSD_SPEC_VERSION] >> 2) & 0xf;
  // Only support spec version > 4.0
  if (spec_vrsn < MMC_CID_SPEC_VRSN_40) {
    return ZX_ERR_NOT_SUPPORTED;
  }

  zxlogf(SPEW, "mmc: CSD version %u spec version %u\n", (raw_csd[MMC_CSD_SPEC_VERSION] >> 6) & 0x3,
         spec_vrsn);
  if (driver_get_log_flags() & DDK_LOG_SPEW) {
    zxlogf(SPEW, "CSD:\n");
    hexdump8_ex(raw_csd, 16, 0);
  }

  // Only support high capacity (> 2GB) cards
  uint16_t c_size = static_cast<uint16_t>(((raw_csd[MMC_CSD_SIZE_START] >> 6) & 0x3) |
                                          (raw_csd[MMC_CSD_SIZE_START + 1] << 2) |
                                          ((raw_csd[MMC_CSD_SIZE_START + 2] & 0x3) << 10));
  if (c_size != 0xfff) {
    zxlogf(ERROR, "mmc: unsupported C_SIZE 0x%04x\n", c_size);
    return ZX_ERR_NOT_SUPPORTED;
  }
  return ZX_OK;
}

}  // namespace

namespace sdmmc {

zx_status_t SdmmcBlockDevice::MmcDoSwitch(uint8_t index, uint8_t value) {
  // Send the MMC_SWITCH command
  zx_status_t st = sdmmc_.MmcSwitch(index, value);
  if (st != ZX_OK) {
    zxlogf(ERROR, "mmc: failed to MMC_SWITCH (0x%x=%d), retcode = %d\n", index, value, st);
    return st;
  }

  // Check status after MMC_SWITCH
  uint32_t resp;
  st = sdmmc_.SdmmcSendStatus(&resp);
  if (st == ZX_OK) {
    if (resp & MMC_STATUS_SWITCH_ERR) {
      zxlogf(ERROR, "mmc: mmc status error after MMC_SWITCH (0x%x=%d), status = 0x%08x\n", index,
             value, resp);
      st = ZX_ERR_INTERNAL;
    }
  } else {
    zxlogf(ERROR, "mmc: failed to MMC_SEND_STATUS (%x=%d), retcode = %d\n", index, value, st);
  }

  return ZX_OK;
}

zx_status_t SdmmcBlockDevice::MmcSetBusWidth(sdmmc_bus_width_t bus_width,
                                             uint8_t mmc_ext_csd_bus_width) {
  // Switch the card to the new bus width
  zx_status_t st = MmcDoSwitch(MMC_EXT_CSD_BUS_WIDTH, mmc_ext_csd_bus_width);
  if (st != ZX_OK) {
    zxlogf(ERROR, "mmc: failed to switch bus width to EXT_CSD %d, retcode = %d\n",
           mmc_ext_csd_bus_width, st);
    return ZX_ERR_INTERNAL;
  }

  if (bus_width != bus_width_) {
    // Switch the host to the new bus width
    if ((st = sdmmc_.host().SetBusWidth(bus_width)) != ZX_OK) {
      zxlogf(ERROR, "mmc: failed to switch the host bus width to %d, retcode = %d\n", bus_width,
             st);
      return ZX_ERR_INTERNAL;
    }
  }
  bus_width_ = bus_width;
  return ZX_OK;
}

uint8_t SdmmcBlockDevice::MmcSelectBusWidth() {
  // TODO verify host 8-bit support
  uint8_t bus_widths[] = {SDMMC_BUS_WIDTH_EIGHT, MMC_EXT_CSD_BUS_WIDTH_8,
                          SDMMC_BUS_WIDTH_FOUR,  MMC_EXT_CSD_BUS_WIDTH_4,
                          SDMMC_BUS_WIDTH_ONE,   MMC_EXT_CSD_BUS_WIDTH_1};
  for (unsigned i = 0; i < (sizeof(bus_widths) / sizeof(uint8_t)); i += 2) {
    if (MmcSetBusWidth(bus_widths[i], bus_widths[i + 1]) == ZX_OK) {
      break;
    }
  }
  return bus_width_;
}

zx_status_t SdmmcBlockDevice::MmcSwitchTiming(sdmmc_timing_t new_timing) {
  // Switch the device timing
  uint8_t ext_csd_timing;
  switch (new_timing) {
    case SDMMC_TIMING_LEGACY:
      ext_csd_timing = MMC_EXT_CSD_HS_TIMING_LEGACY;
      break;
    case SDMMC_TIMING_HS:
      ext_csd_timing = MMC_EXT_CSD_HS_TIMING_HS;
      break;
    case SDMMC_TIMING_HSDDR:
      // sdhci has a different timing constant for HSDDR vs HS
      ext_csd_timing = MMC_EXT_CSD_HS_TIMING_HS;
      break;
    case SDMMC_TIMING_HS200:
      ext_csd_timing = MMC_EXT_CSD_HS_TIMING_HS200;
      break;
    case SDMMC_TIMING_HS400:
      ext_csd_timing = MMC_EXT_CSD_HS_TIMING_HS400;
      break;
    default:
      return ZX_ERR_INVALID_ARGS;
  };

  zx_status_t st = MmcDoSwitch(MMC_EXT_CSD_HS_TIMING, ext_csd_timing);
  if (st != ZX_OK) {
    zxlogf(ERROR, "mmc: failed to switch device timing to %d\n", new_timing);
    return st;
  }

  // Switch the host timing
  if ((st = sdmmc_.host().SetTiming(new_timing)) != ZX_OK) {
    zxlogf(ERROR, "mmc: failed to switch host timing to %d\n", new_timing);
    return st;
  }

  timing_ = new_timing;
  return st;
}

zx_status_t SdmmcBlockDevice::MmcSwitchFreq(uint32_t new_freq) {
  zx_status_t st;
  if ((st = sdmmc_.host().SetBusFreq(new_freq)) != ZX_OK) {
    zxlogf(ERROR, "mmc: failed to set host bus frequency, retcode = %d\n", st);
    return st;
  }
  clock_rate_ = new_freq;
  return ZX_OK;
}

zx_status_t SdmmcBlockDevice::MmcDecodeExtCsd(const uint8_t* raw_ext_csd) {
  zxlogf(SPEW, "mmc: EXT_CSD version %u CSD version %u\n", raw_ext_csd[192], raw_ext_csd[194]);

  // Get the capacity for the card
  uint32_t sectors = (raw_ext_csd[212] << 0) | (raw_ext_csd[213] << 8) | (raw_ext_csd[214] << 16) |
                     (raw_ext_csd[215] << 24);
  block_info_.block_count = sectors * kMmcSectorSize / kMmcBlockSize;
  block_info_.block_size = kMmcBlockSize;

  zxlogf(TRACE, "mmc: found card with capacity = %" PRIu64 "B\n",
         block_info_.block_count * block_info_.block_size);

  return ZX_OK;
}

bool SdmmcBlockDevice::MmcSupportsHs() {
  uint8_t device_type = raw_ext_csd_[MMC_EXT_CSD_DEVICE_TYPE];
  return (device_type & (1 << 1));
}

bool SdmmcBlockDevice::MmcSupportsHsDdr() {
  uint8_t device_type = raw_ext_csd_[MMC_EXT_CSD_DEVICE_TYPE];
  // Only support HSDDR @ 1.8V/3V
  return (device_type & (1 << 2));
}

bool SdmmcBlockDevice::MmcSupportsHs200() {
  uint8_t device_type = raw_ext_csd_[MMC_EXT_CSD_DEVICE_TYPE];
  // Only support HS200 @ 1.8V
  return (device_type & (1 << 4));
}

bool SdmmcBlockDevice::MmcSupportsHs400() {
  uint8_t device_type = raw_ext_csd_[MMC_EXT_CSD_DEVICE_TYPE];
  // Only support HS400 @ 1.8V
  return (device_type & (1 << 6));
}

zx_status_t SdmmcBlockDevice::ProbeMmc() {
  zx_status_t st = ZX_OK;

  // Query OCR
  uint32_t ocr = 0;
  if ((st = sdmmc_.MmcSendOpCond(ocr, &ocr)) != ZX_OK) {
    zxlogf(ERROR, "mmc: MMC_SEND_OP_COND failed, retcode = %d\n", st);
    return st;
  }

  // Indicate sector mode
  if ((st = sdmmc_.MmcSendOpCond(ocr, &ocr)) != ZX_OK) {
    zxlogf(ERROR, "mmc: MMC_SEND_OP_COND failed, retcode = %d\n", st);
    return st;
  }

  // Get CID from card
  // Only supports 1 card currently so no need to loop
  if ((st = sdmmc_.MmcAllSendCid(raw_cid_)) != ZX_OK) {
    zxlogf(ERROR, "mmc: MMC_ALL_SEND_CID failed, retcode = %d\n", st);
    return st;
  }
  zxlogf(SPEW, "mmc: MMC_ALL_SEND_CID cid 0x%08x 0x%08x 0x%08x 0x%08x\n", raw_cid_[0], raw_cid_[1],
         raw_cid_[2], raw_cid_[3]);

  DecodeCid(reinterpret_cast<const uint8_t*>(raw_cid_));

  // Set relative card address
  if ((st = sdmmc_.MmcSetRelativeAddr(1)) != ZX_OK) {
    zxlogf(ERROR, "mmc: MMC_SET_RELATIVE_ADDR failed, retcode = %d\n", st);
    return st;
  }

  // Read CSD register
  if ((st = sdmmc_.MmcSendCsd(raw_csd_)) != ZX_OK) {
    zxlogf(ERROR, "mmc: MMC_SEND_CSD failed, retcode = %d\n", st);
    return st;
  }

  if ((st = DecodeCsd((const uint8_t*)raw_csd_)) != ZX_OK) {
    return st;
  }

  // Select the card
  if ((st = sdmmc_.MmcSelectCard()) != ZX_OK) {
    zxlogf(ERROR, "mmc: MMC_SELECT_CARD failed, retcode = %d\n", st);
    return st;
  }

  // Read extended CSD register
  if ((st = sdmmc_.MmcSendExtCsd(raw_ext_csd_)) != ZX_OK) {
    zxlogf(ERROR, "mmc: MMC_SEND_EXT_CSD failed, retcode = %d\n", st);
    return st;
  }

  if ((st = MmcDecodeExtCsd((const uint8_t*)raw_ext_csd_)) != ZX_OK) {
    return st;
  }
  bus_width_ = SDMMC_BUS_WIDTH_ONE;

  // Switch to high-speed timing
  if (MmcSupportsHs() || MmcSupportsHsDdr() || MmcSupportsHs200()) {
    // Switch to 1.8V signal voltage
    sdmmc_voltage_t new_voltage = SDMMC_VOLTAGE_V180;
    if ((st = sdmmc_.host().SetSignalVoltage(new_voltage)) != ZX_OK) {
      zxlogf(ERROR, "mmc: failed to switch to 1.8V signalling, retcode = %d\n", st);
      return st;
    }

    MmcSelectBusWidth();

    // Must perform tuning at HS200 first if HS400 is supported
    if (MmcSupportsHs200() && bus_width_ != SDMMC_BUS_WIDTH_ONE &&
        !(sdmmc_.host_info().prefs & SDMMC_HOST_PREFS_DISABLE_HS200)) {
      if ((st = MmcSwitchTiming(SDMMC_TIMING_HS200)) != ZX_OK) {
        return st;
      }

      if ((st = MmcSwitchFreq(kFreq200MHz)) != ZX_OK) {
        return st;
      }

      if ((st = sdmmc_.host().PerformTuning(MMC_SEND_TUNING_BLOCK)) != ZX_OK) {
        zxlogf(ERROR, "mmc: tuning failed %d\n", st);
        return st;
      }

      if (MmcSupportsHs400() && bus_width_ == SDMMC_BUS_WIDTH_EIGHT &&
          !(sdmmc_.host_info().prefs & SDMMC_HOST_PREFS_DISABLE_HS400)) {
        if ((st = MmcSwitchTiming(SDMMC_TIMING_HS)) != ZX_OK) {
          return st;
        }

        if ((st = MmcSwitchFreq(kFreq52MHz)) != ZX_OK) {
          return st;
        }

        if ((st = MmcSetBusWidth(SDMMC_BUS_WIDTH_EIGHT, MMC_EXT_CSD_BUS_WIDTH_8_DDR)) != ZX_OK) {
          return st;
        }

        if ((st = MmcSwitchTiming(SDMMC_TIMING_HS400)) != ZX_OK) {
          return st;
        }

        if ((st = MmcSwitchFreq(kFreq200MHz)) != ZX_OK) {
          return st;
        }
      }
    } else {
      if ((st = MmcSwitchTiming(SDMMC_TIMING_HS)) != ZX_OK) {
        return st;
      }

      if (MmcSupportsHsDdr() && (bus_width_ != SDMMC_BUS_WIDTH_ONE)) {
        if ((st = MmcSwitchTiming(SDMMC_TIMING_HSDDR)) != ZX_OK) {
          return st;
        }

        uint8_t mmc_bus_width = (bus_width_ == SDMMC_BUS_WIDTH_FOUR) ? MMC_EXT_CSD_BUS_WIDTH_4_DDR
                                                                     : MMC_EXT_CSD_BUS_WIDTH_8_DDR;
        if ((st = MmcSetBusWidth(bus_width_, mmc_bus_width)) != ZX_OK) {
          return st;
        }
      }

      if ((st = MmcSwitchFreq(kFreq52MHz)) != ZX_OK) {
        return st;
      }
    }
  } else {
    // Set the bus frequency to legacy timing
    if ((st = MmcSwitchFreq(kFreq25MHz)) != ZX_OK) {
      return st;
    }
    timing_ = SDMMC_TIMING_LEGACY;
  }

  zxlogf(INFO, "mmc: initialized mmc @ %u MHz, bus width %d, timing %d\n", clock_rate_ / 1000000,
         bus_width_, timing_);

  return ZX_OK;
}

}  // namespace sdmmc
